<?php
class bbcode_plugin {
    private $txt;
    private $parsed_txt;
    private $config = array(
        'opening_bracket' => '[',
        'closing_bracket' => ']',
    	'closing_sign' => '/',
        'auto_close_tags' => true,
        'bbcodes' => array(),
    );
    private $brackets_length;
    function setConfig( $key, $value ) {
        $this -> config[ $key ] = $value;
    }
    function setText( $txt ) {
        $this -> txt = $txt;
        $this -> parsed_txt = null;
    }
    function cutText( $length, $closeTags = true ) {
        $this -> parsed_txt = $this -> txt;
        $this -> brackets_length = mb_strlen( $this -> config[ 'opening_bracket' ] . $this -> config[ 'closing_bracket' ] );
        
        if( $length < mb_strlen( $this -> parsed_txt ) )
            $this -> parsed_txt = mb_substr( $this -> parsed_txt, 0, mb_strrpos( $this -> parsed_txt, ' ', $length - mb_strlen( $this -> parsed_txt ) ) );
        
        if( $closeTags )
            $this -> close_tags();
        return $this -> parsed_txt;
    }
    
    function getText( ) {
        return $this -> parsed_txt;
    }
    
    function parse( ) {
        if( $this -> parsed_txt === null )
            $this -> parsed_txt = $this -> txt;
        $this -> brackets_length = mb_strlen( $this -> config[ 'opening_bracket' ] . $this -> config[ 'closing_bracket' ] );
        if( $this -> config[ 'auto_close_tags' ] == true )
            $this -> close_tags();
        $this -> _parse();
    }
    private function _parse( ) {
        $stack = array();
        $pos_open = -1;
        $pos_close = null;
        while( ( $pos_open = mb_strpos( $this -> parsed_txt, $this -> config[ 'opening_bracket' ], $pos_open + 1 ) ) !== false ) {
            if( ( $pos_close = mb_strpos( $this -> parsed_txt, $this -> config[ 'closing_bracket' ], $pos_open + 1 ) ) === false )
                break;
            $length = ($pos_close - $pos_open + 1);
            $tag = mb_substr( $this -> parsed_txt, $pos_open, $length );
            $tag_name = $this -> get_tag_name( $tag );
            if( $this -> is_supported( $tag_name ) ) {
                if( $this -> get_type( $tag_name ) === 0 ) {
                    $tmp = mb_substr($this -> parsed_txt, 0, $pos_open);
                    $tmp .= $this -> config[ 'bbcodes' ][ $tag_name ][ 'closing' ];
                    $tmp .= mb_substr( $this -> parsed_txt, $pos_close + 1 );
                    $this -> parsed_txt = $tmp;
                    /*$length = mb_strlen( $this -> config[ 'bbcodes' ][ $tag_name ][ 'closing' ] ) - $length;
                    $pos_close += $length;*/
                }else {
                    if( !$this -> is_closing( $tag ) ) {
                        array_push( $stack, array( 
                            $tag_name => array(
                                'opening' => $pos_open,
                            	'closing' => $pos_close,
                            	'param' => $this -> get_param( $tag ),
                            	'length' => $length
                            ) 
                        ) );
                    }else if( !empty( $stack ) ){
                        $opening = end( $stack );
                        if( $tag_name == key( $opening ) ) {
                            
                            $opening = $opening[ $tag_name ];
                            
                            //otwierajacy tag
                            $tmp = mb_substr( $this -> parsed_txt, 0, $opening[ 'opening' ] );
                            $value = mb_substr( $this -> parsed_txt, $opening[ 'closing' ] + 1, $pos_open - $opening[ 'closing' ] - 1 );
                            $r_what = array( '{{PARAM}}', '{{VALUE}}' );
                            $r_to = array( ($param = $opening[ 'param' ]) !== false ? $param : $value, $value );
                            $tmp .= str_replace( $r_what, $r_to, $this -> config[ 'bbcodes' ][ $tag_name ][ 'opening' ] );
                            
                            //zawartosc
                            $tmp .= $value;
                            
                            //zamykajacy tag
                            $tmp .= $this -> config[ 'bbcodes' ][ $tag_name ][ 'closing' ];
                            $tmp .= mb_substr( $this -> parsed_txt, $pos_close + 1 );
                            
                            //dlugosc
                            /*$length = mb_strlen( $this -> config[ 'bbcodes' ][ $tag_name ][ 'closing' ] ) - $length;
                            $pos_close += $length;*/
                            
                            $this -> parsed_txt = $tmp;
                            
                        }else {
                            array_pop( $stack );
                            $pos_open--;
                            continue;
                        }
                    }
                }
            }
        }
    }
    
    private function close_tags( ) {
        $stack = array();
        $pos_open = -1;
        $pos_close = null;
        while( ( $pos_open = mb_strpos( $this -> parsed_txt, $this -> config[ 'opening_bracket' ], $pos_open + 1 ) ) !== false ) {
            if( ( $pos_close = mb_strpos( $this -> parsed_txt, $this -> config[ 'closing_bracket' ], $pos_open + 1 ) ) === false )
                break;
            $length = ($pos_close - $pos_open + 1);
            $tag = mb_substr( $this -> parsed_txt, $pos_open, $length );
            $tag_name = $this -> get_tag_name( $tag );
            if( $this -> is_supported( $tag_name ) ) {
                if( $this -> get_type( $tag_name ) === 1 ) {
                    if( !$this -> is_closing( $tag ) ) {
                        array_push( $stack, array( 
                            $tag_name => array(
                                'opening' => $pos_open,
                            	'closing' => $pos_close,
                            	'length' => $length
                            ) 
                        ) );
                    }else if( !empty( $stack ) ) {
                        if( $tag_name == key( end( $stack ) ) )
                            array_pop( $stack );
                        else {
                            $tags = '';
                            while( !empty( $stack ) && $tag_name != key( end( $stack ) ) ) {
                                $c = array_pop( $stack );
                                $c = key( $c );
                                $tags .= $this -> config[ 'opening_bracket' ] . $this -> config[ 'closing_sign' ] . $c  . $this -> config[ 'closing_bracket' ];
                            }
                            $tmp = mb_substr( $this -> parsed_txt, 0, $pos_open );
                            $tmp .= $tags;
                            $tmp .= mb_substr( $this -> parsed_txt, $pos_open );
                            $this -> parsed_txt = $tmp;
                        }
                    }
                }
            }
        }
        $tags = '';
        while( !empty( $stack ) ) {
            $c = array_pop( $stack );
            $c = key( $c );
            $tags .= $this -> config[ 'opening_bracket' ] . $this -> config[ 'closing_sign' ] . $c  . $this -> config[ 'closing_bracket' ];
        }
        $this -> parsed_txt .= $tags;
    }
    
    private function get_tag_name( $tag ) {
        $tag = str_replace( $this -> config[ 'closing_sign' ], '', $tag );
        if( ( $len = mb_strpos( $tag, '=' ) - 1 ) === false )
            $len = mb_strlen( $tag ) - $this -> brackets_length;
        return mb_substr( $tag, 1, $len );
    }
    
    private function get_param( $tag ) {
        if( ( $start = mb_strpos( $tag, '="' ) ) === false )
            return false;
        return mb_substr( $tag, $start + 2, -2 );
    }
    
    private function get_type( $tag_name ) {
        return $this -> config[ 'bbcodes' ][ $tag_name ][ 'type' ];
    }
    
    private function is_supported( $tag_name ) {
        if( isset( $this -> config[ 'bbcodes' ][ $tag_name ] ) )
            return true;
        return false;
    }
    
    private function is_closing( $tag ) {
        if( mb_strpos( $tag, $this -> config[ 'closing_sign' ] ) === 1 )
            return true;
        return false;
    }
}